﻿using System;
using System.Collections.Generic;
using System.Diagnostics.Contracts;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

using King.Extensions;

namespace King.IO {

    public abstract class TextReaderBase : TextReader {
        private const int BufferSize = 4096;

        private char[] m_buffer;
        private int m_count;
        private int m_index;
        private bool m_endOfStream;

        private bool BufferEmpty {
            get { return m_count == 0; }
        }
        private async Task<int> BufferSomething() {
            if (m_endOfStream)
                return 0;

            // test if buffer exausted
            if (BufferEmpty) {

                // allocate buffer
                if (m_buffer == null)
                    m_buffer = new char[BufferSize];

                // fill buffer
                m_index = 0;
                m_count = await FillBuffer(m_buffer, m_index, BufferSize);
            }

            // remember if we encountered end-of-stream
            m_endOfStream = BufferEmpty;

            // return count of buffered characters
            return m_count;
        }
        private int UpdateBufferIndexes(int consumed) {
            // update previously buffered indexes
            m_index += consumed;
            m_count -= consumed;

            // return count            
            return consumed;
        }
        private int CopyFromBuffered(StringBuilder stringBuilder, int? count = null) {
            Contract.Requires(!BufferEmpty);
            Contract.Requires(count == null || count <= m_count);

            // copy everything unless a count is specified
            var result = m_count;
            if (count != null)
                result = (int)count;

            // append
            stringBuilder.Append(m_buffer, m_index, result);

            // update indexes
            return UpdateBufferIndexes(result);
        }
        private int CopyFromBuffered(char[] buffer, int index, int count) {
            Contract.Requires(m_count > 0);

            // copy as much as the buffer can hold OR as much as is available
            var result = count < m_count ? count : m_count;

            // copy
            Array.Copy(m_buffer, m_index, buffer, index, result);

            // update indexes
            return UpdateBufferIndexes(result);
        }

        protected abstract Task<int> FillBuffer(char[] buffer, int index, int count);

        public sealed async override Task<int> ReadAsync(char[] buffer, int index, int count) {
            if (await BufferSomething() == 0)
                return 0;

            return CopyFromBuffered(buffer, index, count);
        }
        public sealed async override Task<string> ReadToEndAsync() {
            var stringBuilder = new StringBuilder();

            while (await BufferSomething() > 0)
                CopyFromBuffered(stringBuilder);

            return stringBuilder.ToString();
        }
        public sealed async override Task<string> ReadLineAsync() {
            var stringBuilder = new StringBuilder();

            // return null if end-of-stream reached 
            if (await BufferSomething() == 0)
                return null;

            while (await BufferSomething() > 0) {

                // search for end-of-line
                var maybeEndOfLineIndex = m_buffer.Take(m_count).FirstIndex(o => o == '\r' || o == '\n');

                // found no end-of-line so copy all buffered characters and try again
                if (maybeEndOfLineIndex == null) {
                    CopyFromBuffered(stringBuilder);
                    continue;
                }

                var endOfLineIndex = (int)maybeEndOfLineIndex;
                var endOfLineCount = maybeEndOfLineIndex.Value - m_index;

                // copy characters upto, but not including, the end-of-line character
                CopyFromBuffered(stringBuilder, endOfLineCount);

                // consume '\r' end-of-line character
                if (Peek() == '\r')
                    UpdateBufferIndexes(1);

                // ensure we have something in our buffer or EOF
                await BufferSomething();

                // consume trailing 'n'
                if (Peek() == '\n')
                    UpdateBufferIndexes(1);

                break;
            }

            return stringBuilder.ToString();
        }

        public sealed override string ReadLine() {
            return ReadLineAsync().Result;
        }
        public sealed override int Read(char[] buffer, int index, int count) {
            if (BufferSomething().Result == 0)
                return 0;

            return CopyFromBuffered(buffer, index, count);
        }
        public sealed override int Read() {
            var result = Peek();
            if (result == -1)
                return result;

            UpdateBufferIndexes(1);
            return result;
        }
        public sealed override int Peek() {
            if (BufferSomething().Result == 0)
                return -1;

            return m_buffer[m_index];
        }
    }
}
